iT邦幫忙

2026 iThome 鐵人賽

DAY 17
0
Software Development

文藝復興:這段程式碼,好像有點味道系列 第 17

Day 17|兩本手抄本,永遠要一起翻頁:平行繼承體系 (Parallel Inheritance Hierarchies)

  • 分享至 

  • xImage
  •  

畫室的花名冊,通常不只一本

一本記著「誰是誰的徒弟」,另一本記著「誰負責哪一種顏料的調配權限」

兩本冊子,乍看是獨立的紀錄,但仔細一看
每收一位新學徒,兩本冊子都要各補一行,而且順序完全對應

師傅收徒弟時,總是同時想著:「這個名字,要記兩次」

通知管道,長出了第二套結構

Day 09 建立了 INotificationChannel 的繼承體系:EmailNotificationChannelSmsNotificationChannelPushNotificationChannel

團隊後來加了一個「發送頻率限制」的需求
同一位客戶,每種管道都有各自的發送上限(Email 一天 5 封、簡訊一天 2 則、推播不限)」

有人選擇另外開一套繼承體系來管理限制邏輯:

public abstract class ChannelRateLimiter
{
    public abstract bool IsWithinLimit(Customer customer);
}

public class EmailRateLimiter : ChannelRateLimiter
{
    public override bool IsWithinLimit(Customer customer) => customer.EmailSentToday < 5;
}

public class SmsRateLimiter : ChannelRateLimiter
{
    public override bool IsWithinLimit(Customer customer) => customer.SmsSentToday < 2;
}

public class PushRateLimiter : ChannelRateLimiter
{
    public override bool IsWithinLimit(Customer customer) => true;
}

三個管道類別,配上三個限制器類別,兩套繼承體系,結構完全對稱

呼叫端,被迫記住兩本冊子的對應關係

發送通知的地方,得同時操作兩套體系:

public void NotifyCustomer(Customer customer, INotificationChannel channel)
{
    ChannelRateLimiter limiter = channel switch
    {
        EmailNotificationChannel => new EmailRateLimiter(),
        SmsNotificationChannel => new SmsRateLimiter(),
        PushNotificationChannel => new PushRateLimiter(),
        _ => throw new ArgumentException("未知的通知管道")
    };

    if (limiter.IsWithinLimit(customer))
        channel.Send(customer);
}

這段 switch,其實不是新問題,它是 Day 09 才處理掉的舊症狀,悄悄從另一個入口溜了回來

更麻煩的是,半年後要加 LINE 通知(Day 12 的 LineNotificationChannel),意味著:

  1. 已經知道要新增一個 LineNotificationChannel
  2. 還要記得同步新增一個 LineRateLimiter
  3. 還要記得回來改這段 switch,把兩者配對起來

新增一種管道,變成了三件事,而且第二、第三件事,很容易忘記

兩本冊子,其實該合併成一本

解法的核心思想:「這個管道每天最多能發幾次」,跟「這個管道怎麼發送」,其實是同一個物件該回答的兩個問題,不該拆成兩個平行的類別體系

換成程式碼,這是把 RateLimiter 體系的職責,搬移INotificationChannel 自己身上:

public interface INotificationChannel
{
    void Send(Customer customer);
    string Preview(Customer customer);
    bool IsWithinLimit(Customer customer);
}

每個管道類別,自己回答自己的限制邏輯:

public class EmailNotificationChannel : INotificationChannel
{
    private readonly IEmailService _emailService;
    public EmailNotificationChannel(IEmailService emailService) => _emailService = emailService;

    public void Send(Customer customer) =>
        _emailService.Send(customer.Email, "Order Confirmed", Preview(customer));

    public string Preview(Customer customer) => $"親愛的 {customer.Name},您的訂單已確認。";

    public bool IsWithinLimit(Customer customer) => customer.EmailSentToday < 5;
}

public class SmsNotificationChannel : INotificationChannel
{
    private readonly ISmsService _smsService;
    public SmsNotificationChannel(ISmsService smsService) => _smsService = smsService;

    public void Send(Customer customer) => _smsService.Send(customer.Phone, Preview(customer));
    public string Preview(Customer customer) => $"訂單確認:{customer.Name} 您好。";
    public bool IsWithinLimit(Customer customer) => customer.SmsSentToday < 2;
}

ChannelRateLimiter 整套體系可以直接刪除

呼叫端恢復乾淨,switch 也一併消失:

public void NotifyCustomer(Customer customer, INotificationChannel channel)
{
    if (channel.IsWithinLimit(customer))
        channel.Send(customer);
}

未來新增 LineNotificationChannel,只需要在這一個類別裡,把 SendPreviewIsWithinLimit 都寫好,就不用還要提心吊膽還有哪個地方沒改到

什麼時候,兩套結構真的該分開

不是所有「看起來對應」的兩套類別,都該合併:

  • 如果兩套結構描述的是完全不同的概念
    • 例如「員工職級」跟「辦公室座位」,就算數量剛好一樣,也不代表該合併
  • 真正該警覺的訊號是,每次替其中一邊新增子類別,你會不假思索地說出「所以另一邊也要加一個」,這句話才是平行繼承體系現形的時刻

模組三,蓋完了

從 Day 15 到 Day 17,我們看到三種「改動變貴」的方式:

  • 一個類別,扛了太多互不相關的理由(發散式變更)
  • 一個理由,被拆散到太多不相關的類別(霰彈式修改)
  • 兩套結構,被迫永遠同步增減(平行繼承體系)

三者的解法看似不同,提煉、搬移、合併,但目標始終一致(讓每一種變化的理由,都只對應到一個修改的地方)

自我檢查清單

  1. 系統裡有沒有兩組類別,每次其中一邊新增子類別,另一邊也要跟著新增?
  2. 我有沒有說過「加了這個,別忘了也要加那個」這種話?
  3. 這兩套結構,是不是其實在描述同一個物件的不同面向?
  4. 如果把其中一套結構的職責搬到另一套裡,呼叫端會不會變得更簡單?
  5. 合併之後,還需要一段 switchif/is 來配對兩者嗎?

明日預告

圓頂立起來了,透視法找到了,灰泥的時間壓力也想清楚了
明天我們前往佛羅倫斯的另一位大師,阿伯提
去看看他留給我們的節制美學:多餘的裝飾,不是美,是負擔

模組四,正式開工


上一篇
Day 16|換一種顏料,整間畫室都要重新調色:霰彈式修改 (Shotgun Surgery)
下一篇
Day 18|灰泥要乾之前:現在修,還是先記一筆技術債?
系列文
文藝復興:這段程式碼,好像有點味道27
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言